/**
 * A星寻路管理器
 */
import AStarNode, { AstarNodeType } from "./AStarNode";

export default class AStarMgr {
    static singleInstance: AStarMgr = null;
    static getInstance(): AStarMgr {
        if (AStarMgr.singleInstance == null) {
            AStarMgr.singleInstance = new AStarMgr();
        }
        return AStarMgr.singleInstance;
    }

    public mapW: number;
    public mapH: number;
    private nodeW: number;
    private nodeH: number;
    public nodes: Array<Array<AStarNode>>;
    private openList: Set<AStarNode>;
    private closeList: Set<AStarNode>;
    private openArray: Array<AStarNode>;
    
    public initNapInfo(mapW: number, mapH: number, nodeW: number, nodeH: number): void {
        this.mapW = Math.floor(mapW / nodeW);
        this.mapH = Math.floor(mapH / nodeH);
        this.nodeW = nodeW;
        this.nodeH = nodeH;
        this.openList = new Set<AStarNode>();
        this.closeList = new Set<AStarNode>();
        this.nodes = [];
        for (let i = 0; i < mapW; i++) {
            this.nodes[i] = [];
            for (let j = 0; j < mapH; j++) {
                // 暂时随机生成阻挡
                let node = new AStarNode(
                    i, j, 
                    Math.random() < 0.2? AstarNodeType.OBSTACLES : AstarNodeType.ROAD
                );
                this.nodes[i][j] = node; 
            }
        }
    }

    private getNodeByPos(x: number, y: number): AStarNode {
        try {
            if (this.isOutBoundary(x, y)) return;
            return this.nodes[x][y];
        } catch (error) {
            console.error("getNodeByPos", x, y, JSON.stringify(this.nodes), error);
        }
    }

    public isOutBoundary(x: number, y: number): boolean {
        if (x < 0 || x >= this.mapW ||
            y < 0 || y >= this.mapH) {
            return  true;
        }
        return false;
    }

    private putNearlyNodeToOpenlist(x: number, y: number, g: number, father: AStarNode, end: AStarNode, associatedNode1?: AStarNode, associatedNode2?: AStarNode) {
        if (this.isOutBoundary(x, y)) return;
        let node: AStarNode = this.getNodeByPos(x, y);
        if (!node || node.type === AstarNodeType.OBSTACLES ||
            this.openList.has(node) ||
            this.closeList.has(node)
        ) {
            return;
        };

        if (associatedNode1 && associatedNode2) {
            if (associatedNode1.type === AstarNodeType.OBSTACLES &&
                associatedNode1.type === AstarNodeType.OBSTACLES) {
                return;
            }
        }

        if (associatedNode1 && !associatedNode2) {
            if (associatedNode1.type === AstarNodeType.OBSTACLES) return;
        }

        if (!associatedNode1 && associatedNode2) {
            if (associatedNode2.type === AstarNodeType.OBSTACLES) return;
        }
        
        
        node.father = father;
        // 距离起点的距离就是父亲节点离起点的距离加我离父节点的距离
        node.g = father.g + g;
        node.h = Math.abs(end.x - node.x) + Math.abs(end.y - node.y);
        node.f = node.g + node.h;

        this.openList.add(node);
        this.openArray.push(node);
    }
    
    public findPath(startPos: cc.Vec2, endPos: cc.Vec2): Array<AStarNode> | void {
        /**
         * 1 判断传入的点是否合法
         * 2 找周围的点，左上、上、右上、左、右、左下、下、右下，
         * 注意是否是边界，是否在开启列表，或关闭列表、不在才能放到开启列表
         * 3 选出开启列表中寻路消耗最小的移除出开启列表，放入关闭列表中
         * 4 如果这个点已经是终点，返回最终路径，如果不是终点，继续寻路
         */
        if (this.isOutBoundary(startPos.x, startPos.y) ||
            this.isOutBoundary(endPos.x, endPos.y)) {
            console.error("开始或者结束点位置不在地图范围内!");
            return;
        }

        let startNode: AStarNode = this.getNodeByPos(startPos.x, startPos.y);
        let endNode: AStarNode = this.getNodeByPos(endPos.x, endPos.y);
        if ((!startNode || startNode.type === AstarNodeType.OBSTACLES) || (!endNode ||
            endNode.type === AstarNodeType.OBSTACLES)) {
            console.error("开始或者结束点是障碍物!");
            return;
        }

        this.openList.clear();
        this.closeList.clear();
        this.openArray = [];

        startNode.father = null;
        startNode.f = 0;
        startNode.g = 0;
        startNode.h = 0;
        this.closeList.add(startNode);

        while(true) {
            this.putNearlyNodeToOpenlist(
                startNode.x - 1, startNode.y - 1, 1.4, startNode, endNode, 
                this.getNodeByPos(startNode.x - 1, startNode.y),
                this.getNodeByPos(startNode.x, startNode.y - 1)
            );
            this.putNearlyNodeToOpenlist(startNode.x, startNode.y - 1, 1, startNode, endNode);
            this.putNearlyNodeToOpenlist(
                startNode.x + 1, startNode.y - 1, 1.4, startNode, endNode,
                this.getNodeByPos(startNode.x + 1, startNode.y),
                this.getNodeByPos(startNode.x, startNode.y - 1)
            );
            this.putNearlyNodeToOpenlist(startNode.x - 1, startNode.y, 1, startNode, endNode);
            this.putNearlyNodeToOpenlist(startNode.x + 1, startNode.y, 1, startNode, endNode);
            this.putNearlyNodeToOpenlist(
                startNode.x - 1, startNode.y + 1, 1.4, startNode, endNode,
                this.getNodeByPos(startNode.x - 1, startNode.y),
                this.getNodeByPos(startNode.x, startNode.y + 1)
            );
            this.putNearlyNodeToOpenlist(startNode.x, startNode.y + 1, 1, startNode, endNode);
            this.putNearlyNodeToOpenlist(
                startNode.x + 1, startNode.y + 1, 1.4, startNode, endNode,
                this.getNodeByPos(startNode.x + 1, startNode.y),
                this.getNodeByPos(startNode.x, startNode.y + 1)
            );
            if (this.openList.size === 0) {
                console.error("没找到路径!");
                return;
            }

            this.openArray.sort((a, b) => {
                return a.f - b.f;
            });
            startNode = this.openArray[0];
            this.openArray.splice(0, 1);
            this.closeList.add(startNode);
            this.openList.delete(startNode);

            if (startNode === endNode) {
                let path = [endNode];
                while(endNode.father) {
                    path.push(endNode);
                    endNode = endNode.father;
                }
                return path.reverse();
            }
        }
    }
}
